﻿using System;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace vm.Aspects.Security.Cryptography.Tests
{
    [TestClass]
    public class DpapiCipherTest
    {
        public TestContext TestContext { get; set; }

        public ICipherAsync GetCipher(bool base64 = false)
        {
            return new DpapiCipher();
        }

        [TestMethod]
        public void EncryptNullTest()
        {
            using (var target = GetCipher())
            {
                var actual = target.Encrypt(null);

                Assert.IsNull(actual);
            }
        }

        [TestMethod]
        public void Encrypt0LengthTest()
        {
            using (var target = GetCipher())
            {
                var expected = new byte[0];
                var actual = target.Encrypt(new byte[0]);

                Assert.IsFalse(actual.SequenceEqual(expected));
            }
        }

        [TestMethod]
        public void EncryptTest()
        {
            using (var target = GetCipher())
            {
                var input = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
                var actual = target.Encrypt(input);

                Assert.IsFalse(actual.SequenceEqual(input));
            }
        }

        [TestMethod]
        public void DecryptNullTest()
        {
            using (var target = GetCipher())
            {
                var actual = target.Decrypt(null);

                Assert.IsNull(actual);
            }
        }

        [TestMethod]
        [ExpectedException(typeof(CryptographicException))]
        public void Decrypt0LengthTest()
        {
            using (var target = GetCipher())
            {
                var expected = new byte[0];
                var actual = target.Decrypt(new byte[0]);
            }
        }

        [TestMethod]
        public void RoundTripTest()
        {
            using (var target = GetCipher())
            {
                var input = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
                var encrypted = target.Encrypt(input);
                var output =  target.Decrypt(encrypted);

                Assert.IsTrue(input.SequenceEqual(output));
            }
        }

        [TestMethod]
        public void RoundTripLocalUserTest()
        {
            using (var target = GetCipher())
            {
                ((DpapiCipher)target).Scope = DataProtectionScope.CurrentUser;

                var input = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
                var encrypted = target.Encrypt(input);
                var output =  target.Decrypt(encrypted);

                Assert.IsTrue(input.SequenceEqual(output));
            }
        }

        [TestMethod]
        public void RoundTripWithEntropyTest()
        {
            using (var target = GetCipher())
            {

                ((DpapiCipher)target).Entropy = Encoding.Unicode.GetBytes("CosmicEntropy");

                var input = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
                var encrypted = target.Encrypt(input);
                var output =  target.Decrypt(encrypted);

                Assert.IsTrue(input.SequenceEqual(output));
            }
        }

        [TestMethod]
        public void RoundTripWithEntropyLocalUserTest()
        {
            using (var target = GetCipher())
            {

                ((DpapiCipher)target).Scope = DataProtectionScope.CurrentUser;
                ((DpapiCipher)target).Entropy = Encoding.Unicode.GetBytes("CosmicEntropy");

                var input = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
                var encrypted = target.Encrypt(input);
                var output =  target.Decrypt(encrypted);

                Assert.IsTrue(input.SequenceEqual(output));
            }
        }

        [TestMethod]
        [ExpectedException(typeof(ArgumentNullException))]
        public void EncryptStreamNullInput()
        {
            using (var target = GetCipher())
            {
                var input = (Stream)null;
                var output = new MemoryStream();

                target.Encrypt(input, output);
            }
        }

        [TestMethod]
        [ExpectedException(typeof(ArgumentNullException))]
        public void EncryptStreamNullOutput()
        {
            using (var target = GetCipher())
            {
                var input = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }, false);
                var output = (Stream)null;

                target.Encrypt(input, output);
            }
        }

        [TestMethod]
        [ExpectedException(typeof(ArgumentException))]
        public void EncryptStreamReadableOutput()
        {
            using (var target = GetCipher())
            {
                var input = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }, false);
                var output = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }, false);

                target.Encrypt(input, output);
            }
        }


        [TestMethod]
        [ExpectedException(typeof(ArgumentNullException))]
        public void DecryptStreamNullInput()
        {
            using (var target = GetCipher())
            {
                var input = (Stream)null;
                var output = new MemoryStream();

                target.Decrypt(input, output);
            }
        }

        [TestMethod]
        [ExpectedException(typeof(ArgumentNullException))]
        public void DecryptStreamNullOutput()
        {
            using (var target = GetCipher())
            {
                var input = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }, false);
                var output = (Stream)null;

                target.Decrypt(input, output);
            }
        }

        [TestMethod]
        [ExpectedException(typeof(ArgumentException))]
        public void DecryptStreamReadableOutput()
        {
            using (var target = GetCipher())
            {
                var input = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }, false);
                var output = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }, false);

                target.Decrypt(input, output);
            }
        }

        [TestMethod]
        [ExpectedException(typeof(InvalidOperationException))]
        public void DecryptBadLength()
        {
            using (var target = GetCipher())
            {
                var inputData = new byte[100].FillRandom();
                var input = new MemoryStream(inputData);
                var output = new MemoryStream();

                target.Encrypt(input, output);

                input.Seek(0, SeekOrigin.Begin);
                output.Seek(0, SeekOrigin.Begin);

                var outputData = output.ToArray().Take(3).ToArray();
                var decrypted = new MemoryStream();

                using (var target1 = GetCipher())
                    target1.Decrypt(new MemoryStream(outputData), decrypted);
            }
        }

        [TestMethod]
        [ExpectedException(typeof(InvalidOperationException))]
        public void DecryptBadBuffer()
        {
            using (var target = GetCipher())
            {
                var inputData = new byte[100].FillRandom();
                var input = new MemoryStream(inputData);
                var output = new MemoryStream();

                target.Encrypt(input, output);

                input.Seek(0, SeekOrigin.Begin);
                output.Seek(0, SeekOrigin.Begin);

                var outputData = output.ToArray().Take(50).ToArray();
                var decrypted = new MemoryStream();

                using (var target1 = GetCipher())
                    target1.Decrypt(new MemoryStream(outputData), decrypted);
            }
        }

        void ParameterizedEncryptStream(int length)
        {
            using (var target = GetCipher())
            {
                var inputData = new byte[length].FillRandom();
                var input = new MemoryStream(inputData);
                var output = new MemoryStream();

                target.Encrypt(input, output);
                output.Close();

                var outputData = output.ToArray();

                Assert.IsFalse(inputData.SequenceEqual(outputData));
                Assert.IsTrue(inputData.Length < outputData.Length);
            }
        }

        void ParameterizedRoundTrip(int length)
        {
            using (var target = GetCipher())
            {
                var inputData = new byte[length].FillRandom();
                var input = new MemoryStream(inputData);
                var output = new MemoryStream();

                target.Encrypt(input, output);

                input.Seek(0, SeekOrigin.Begin);
                output.Seek(0, SeekOrigin.Begin);

                var outputData = output.ToArray();

                Assert.IsFalse(inputData.SequenceEqual(outputData));
                Assert.IsTrue(inputData.Length < outputData.Length);

                var decrypted = new MemoryStream();

                using (var target1 = GetCipher())
                {
                    target1.Decrypt(output, decrypted);
                    decrypted.Close();

                    var decryptedData = decrypted.ToArray();

                    Assert.IsTrue(inputData.SequenceEqual(decryptedData));
                }
            }
        }

        [TestMethod]
        public void EncryptStream0()
        {
            ParameterizedEncryptStream(0);
        }

        [TestMethod]
        public void RoundTripStream0()
        {
            ParameterizedRoundTrip(0);
        }

        [TestMethod]
        public void EncryptStreamLessThan4k()
        {
            ParameterizedEncryptStream(1024);
        }

        [TestMethod]
        public void RoundTripStreamLessThan4k()
        {
            ParameterizedRoundTrip(1024);
        }

        [TestMethod]
        public void EncryptStream4k()
        {
            ParameterizedEncryptStream(4096);
        }

        [TestMethod]
        public void RoundTripStream4k()
        {
            ParameterizedRoundTrip(4096);
        }

        [TestMethod]
        public void EncryptStreamNx4k()
        {
            ParameterizedEncryptStream(3*4096);
        }

        [TestMethod]
        public void RoundTripStreamNx4k()
        {
            ParameterizedRoundTrip(3*4096);
        }

        [TestMethod]
        public void EncryptStreamMoreThanNx4k()
        {
            ParameterizedEncryptStream(3*4096+734);
        }

        [TestMethod]
        public void RoundTripStreamMoreThanNx4k()
        {
            ParameterizedRoundTrip(3*4096+734);
        }
    }
}
